﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Numerics;

namespace SafeCalculations.SafeTypes
{
    /// <summary>
    /// Represents a large (but not unbounded) integer value and handles addition in terms of int32 instances to keep performance high.
    /// </summary>
    public struct Int32WithBoundedRollover
    {
        /// <summary>
        /// The current <see cref="int"/> value of the integer minus any overflow/underflow information.
        /// </summary>
        private int _partialValue;

        /// <summary>
        /// Stores the number of numeric overflows/underflows that would need to be added to construct the full integer value.
        /// </summary>
        private int _overflows;

        /// <summary>
        /// Returns true if the value represented would fit into an <see cref="int"/>.
        /// </summary>
        public bool FitsIntoInt32 { get { return this._overflows == 0; } }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="startingValues">Collection of values that will be immediately added to this integer value.</param>
        public Int32WithBoundedRollover(params int[] startingValues)
            : this()
        {
            if (startingValues != null)
                this.Add(startingValues);
        }

        /// <summary>
        /// Adds a collection of values to the <see cref="Int32WithBoundedRollover"/>. Only a collection based overload is provided because adding single values would require a separate function call for each value and degrade performance.
        /// </summary>
        /// <param name="values"></param>
        public void Add(int[] values)
        { this.Add(values, 0, (uint)values.Length); }

        /// <summary>
        /// Adds a subset of a collection of values to the <see cref="Int32WithBoundedRollover"/>. Only a collection based overload is provided because adding single values would require a separate function call for each value and degrade performance.
        /// </summary>
        /// <param name="valuesBuffer"></param>
        /// <param name="startIndex">The offset of the first value to add from <see cref="valuesBuffer"/>.</param>
        /// <param name="numValues">The total number of values to add from <see cref="valuesBuffer"/> beginning from <see cref="startIndex"/>.</param>
        public void Add(int[] valuesBuffer, uint startIndex, uint numValues)
        {
            if (valuesBuffer == null)
                throw new ArgumentNullException("valuesBuffer");
            if (startIndex < 0 || startIndex >= valuesBuffer.Length)
                throw new ArgumentOutOfRangeException("startIndex");
            if (numValues + startIndex > valuesBuffer.Length)
                throw new ArgumentOutOfRangeException("numValues + startIndex");

            uint onePastFinalIndex = startIndex + numValues;
            for (uint i = startIndex; i < onePastFinalIndex; i++)
            {
                //Add the new value to the current value and check for an overflow, if an overflow occured update the overflow counter
                int previousValue = _partialValue;
                int newItemToAdd = valuesBuffer[i];

                unchecked { this._partialValue += newItemToAdd; }

                //Check to see if there was an overflow using bit manipulation magic
                if (((newItemToAdd ^ this._partialValue) & (previousValue ^ this._partialValue)) < 0)
                { this._overflows += Math.Sign(previousValue); }
            }
        }

        /// <summary>
        /// Converts the value to an <see cref="int"/>. This will only work if <see cref="FitsIntoInt32"/> returns true, otherwise an exception will be thrown. If <see cref="FitsIntoInt32"/> returns false use <see cref="ToBigInteger"/> instead.
        /// </summary>
        /// <returns></returns>
        public int ToInt32()
        {
            if (!this.FitsIntoInt32)
                throw new InvalidOperationException("Can not convert the instance of \"{0}\" to an int32 because it won't fit.");

            return this._partialValue;
        }

        /// <summary>
        /// Converts the value to a <see cref="BigInteger"/>. If <see cref="FitsIntoInt32"/> returns true use <see cref="ToInt32"/> instead to increase performance.
        /// </summary>
        /// <returns></returns>
        public BigInteger ToBigInteger()
        {
            if (this._overflows == 0) { return this._partialValue; }
            else
            {
                //The total is the sum of the value of all full overflows, the half overflow (first overflow), and the remainder

                int overflowsSign = (this._overflows > 0) ? 1 : -1;

                //First start out with the value of the half overflow (the first overflow only covers either int.MinValue or int.MaxValue)
                BigInteger value = (this._overflows > 0) ? int.MaxValue : int.MinValue;
                value += overflowsSign;
                
                //Second add in the full overflows, where the full overflows is one less in magnitude from the total number of overflows
                int signedNumFullOverflows = this._overflows - overflowsSign;
                if (signedNumFullOverflows != 0)
                {
                    //Make the value negative for ease of calculations. The negative of int.MinValue is an overflow since int.MinValue has a larger magnitude than int.MaxValue by one.
                    BigInteger fullOverflows = int.MinValue;
                    fullOverflows += int.MinValue;

                    //It is safe to switch the sign of signedNumFullOverflows because it was decremented in magnitude by one at its creation so it will fit into an int32.
                    fullOverflows *= -signedNumFullOverflows;
                    
                    value += fullOverflows;
                }

                //Third add in the remainder, first check to see if a regular int32 can be used and if not resort to a big integer
                if (this._overflows < 0 && this._partialValue > -1)
                    value += this._partialValue - int.MaxValue;
                else if (this._overflows > 0 && this._partialValue <= -1)
                    value += (this._partialValue + int.MaxValue) + 1;
                else
                {
                    BigInteger remainder = (this._overflows > 0) ? int.MinValue : int.MaxValue;
                    remainder = this._partialValue - remainder;
                    value += remainder;
                }

                return value;
            }
        }
    }
}
